"""Parser for the owners.yaml file."""
import argparse
import fnmatch
import json
import os
import pathlib
import re
import typing

import sentry_sdk

from . import misc
from . import yaml
from .logger import get_logger

LOGGER = get_logger(__name__)


class Subsystem:
    """Represents a subsystem in the owners.yaml file."""

    def __init__(self, data):
        """Save an individual subsystem from the Parser class."""
        self._data = data

    @property
    def external_tests(self):
        """Return a list of readyForMergeDeps data."""
        return misc.get_nested_key(self._data, 'labels/readyForMergeDeps') or []

    @property
    def subsystem_name(self):
        """Return the subsystem name as a string."""
        return misc.get_nested_key(self._data, 'subsystem', '')

    @property
    def subsystem_label(self):
        """Return the subsystem label as a string."""
        return misc.get_nested_key(self._data, 'labels/name', '')

    @property
    def ready_for_merge_label_deps(self):
        """Return the list of ready for merge dep labels, if any."""
        result = []
        for external_test in self.external_tests:
            if isinstance(external_test, str):
                result.append(external_test)
            else:
                try:
                    result.append(external_test['name'])
                except KeyError:
                    LOGGER.debug("Invalid external_test: %s. Missing 'name' key.",
                                 external_test)
                    return []
        return result

    @property
    def status(self):
        """Return the status."""
        return misc.get_nested_key(self._data, 'status', '')

    @property
    def required_approvals(self):
        """Return the requiredApproval value if set, otherwise False."""
        return misc.get_nested_key(self._data, 'requiredApproval', False)

    @property
    def maintainers(self):
        """Return the list of maintainers."""
        return list(misc.flattened(misc.get_nested_key(self._data, 'maintainers') or []))

    @property
    def reviewers(self):
        """Return the list of reviewers."""
        return list(misc.flattened(misc.get_nested_key(self._data, 'reviewers') or []))

    @property
    def scm(self):
        """Return the SCM string."""
        return misc.get_nested_key(self._data, 'scm', '')

    @property
    def mailing_list(self):
        """Return the mailinglist."""
        return misc.get_nested_key(self._data, 'mailingList', '')

    @property
    def jira_component(self):
        """Return the proper jira component."""
        return misc.get_nested_key(self._data, 'jiraComponent', '')

    @property
    def devel_sst(self):
        """Return the development subsystem team."""
        return misc.get_nested_key(self._data, 'devel-sst', '')

    @property
    def qe_sst(self):
        """Return the QE subsystem team."""
        return misc.get_nested_key(self._data, 'qe-sst', '')

    @property
    def test_variants(self):
        """Return the list of kernel variants requesting extra testing, if any."""
        return misc.get_nested_key(self._data, 'testVariants') or []

    @property
    def path_include_regexes(self):
        """Return the list of include path regexes."""
        return misc.get_nested_key(self._data, 'paths/includeRegexes') or []

    @property
    def path_includes(self):
        """Return the list of include paths."""
        return misc.get_nested_key(self._data, 'paths/includes') or []

    @property
    def path_excludes(self):
        """Return the list of exclude paths."""
        return misc.get_nested_key(self._data, 'paths/excludes') or []

    @staticmethod
    def _glob_matches(filename, patterns):
        """Perform a glob match against multiple patterns."""
        return any(glob_match(filename, x) for x in patterns)

    @staticmethod
    def _regex_matches(filename, regexes):
        """Perform a regex match against multiple patterns."""
        return any(not x or re.search(x, filename) for x in regexes)

    def _perform_match(self, filename):
        return (
            self._glob_matches(filename, self.path_includes) or
            self._regex_matches(filename, self.path_include_regexes)
        ) and not self._glob_matches(filename, self.path_excludes)

    def matches(self, filenames):
        """Determine if any of the filenames match the path includes and excludes."""
        return any(self._perform_match(x) for x in filenames)

    def __repr__(self):
        """Return the subsystem name as the string repesentation of the Entry class."""
        return self.subsystem_name


class Parser:
    """Provide access to an owners.yaml file."""

    def __init__(self, config):
        """Initialize the class with the config from an owners file."""
        self.subsystems = [Subsystem(s) for s in (config or {}).get('subsystems', [])]

    def get_matching_subsystems(self, filenames):
        """Return all of the matching entries in the owners.yaml file."""
        return [x for x in self.subsystems if x.matches(filenames)]

    def get_matching_subsystems_by_label(self, label):
        """Return all the entries matching the given label name."""
        return [x for x in self.subsystems if x.subsystem_label == label]

    def get_matching_subsystem_by_name(self, name: str) -> Subsystem | None:
        """Return the Subsystem with the matching subsystem name, or None."""
        return next((x for x in self.subsystems if x.subsystem_name == name), None)


def glob_match(filename, pattern):
    """Check whether the filename matches the given shell-style pattern.

    Unlike fnmatch.fnmatchcase, the directories are matched correctly.
    """
    # Special case: a pattern ending with a slash matches everything under
    # the given directory.
    dir_match = False
    if pattern.endswith('/'):
        pattern = pattern[:-1]
        dir_match = True

    # Split to path components.
    fn_components = filename.split('/')
    pat_components = pattern.split('/')

    # If the pattern has more components, there is no way the filename can
    # match.
    if len(fn_components) < len(pat_components):
        return False

    # Check the path components one by one.
    for fn_one, pat_one in zip(fn_components, pat_components):
        if not fnmatch.fnmatchcase(fn_one, pat_one):
            return False

    # If we're matching everything under the pattern directory, it's okay if
    # there are extra components in the path. This is a match.
    if dir_match:
        return True

    # We may have more components in the filename than in the pattern. This
    # should generally not happen. Let's be conservative and return no
    # match in such case.
    return len(fn_components) == len(pat_components)


def main(argv: typing.Optional[typing.List[str]] = None) -> None:
    """Print responsible subsystems for kernel source files."""
    parser = argparse.ArgumentParser(
        description='Print responsible subsystems for kernel source files')
    parser.add_argument('--owners-yaml', default=os.environ.get('OWNERS_YAML'),
                        help='contents of owners.yaml (default: env[OWNERS_YAML])')
    parser.add_argument('--owners-yaml-path', default=os.environ.get('OWNERS_YAML_PATH'),
                        help='path of owners.yaml (default: env[OWNERS_YAML_PATH])')
    parser.add_argument('--output', default='text', choices=('json', 'text'),
                        help='output format (default: text)')
    parser.add_argument('--file-list',
                        help='file with kernel source file names, one per line')
    parser.add_argument('files', default=[], nargs='*',
                        help='kernel source files')
    args = parser.parse_args(argv)

    owners_parser = Parser(yaml.load(contents=args.owners_yaml, file_path=args.owners_yaml_path))
    files = set(args.files)
    if args.file_list:
        files |= {f for f in pathlib.Path(args.file_list).read_text('utf8').split('\n') if f}

    if args.output == 'text':
        subsystems = {
            f'@{s.subsystem_label}\n'
            for s in owners_parser.get_matching_subsystems(files)
            if s.subsystem_label
        }
        subsystem_files = {
            f'@{s.subsystem_label}/{f}\n'
            for f in files
            for s in owners_parser.get_matching_subsystems([f])
            if s.subsystem_label
        }
        return ''.join(sorted(subsystems | subsystem_files))
    if args.output == 'json':
        subsystem_data = [
            {'name': s.subsystem_name, 'label': s.subsystem_label}
            for s in owners_parser.get_matching_subsystems(files)
            if s.subsystem_label
        ]
        return json.dumps(sorted(subsystem_data, key=lambda s: s['label']), indent=2) + '\n'
    raise ValueError(f'Unknown output format {args.output}')


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    print(main(), end='')
